Skip to content

Spatially-distributed channel subsets for the sweeps (#91) - #99

Merged
neuromechanist merged 3 commits into
mainfrom
feature/issue-91-distributed-channels
Jul 11, 2026
Merged

Spatially-distributed channel subsets for the sweeps (#91)#99
neuromechanist merged 3 commits into
mainfrom
feature/issue-91-distributed-channels

Conversation

@neuromechanist

Copy link
Copy Markdown
Member

Closes #91.

The dimension sweeps reduced channel count by slicing full[:nc] -- the first nc electrodes in file order, which for ds002718 (EEG001..) is a spatial cluster (one scalp region), not a whole-head cap. So reduced-channel decompositions and their IC scalp maps were not physically meaningful reduced montages (visible in the Phase 3 16ch topomaps, #87).

What changed

  • benchmarks/channel_selection.py (new): parse a BIDS electrodes.tsv and pick nc spatially-distributed channels via greedy farthest-point (k-center) sampling over the real 3D electrode positions. Deterministic (seeded from the centroid-nearest electrode). Unlocalized (n/a) channels are excluded.
  • benchmark_decompose.py: when --montage is given, reduced channel counts use the distributed subset for the decomposition; the selected channel indices are saved in each npz and threaded through _compare/_plot_topomaps/_load_info so the topomaps and the de-sphere use the correct electrodes (not EEG001..EEG_nc).
  • benchmark_dimsweep.py: same --montage distributed selection at the slice (channel count unchanged, so timing is unaffected; only the montage becomes physical).
  • pyproject.toml: new viz extra (mne) for the topomap/montage path, with uv.lock updated.

Tests

pyAMICA/tests/test_channel_selection.py (6 tests) against a real committed electrode fixture (ds002718 sub-002, CC0): parsing, deterministic/unique/sorted selection, and the key property that a distributed subset is more spread (larger nearest-pair distance) than the first-N cluster. Also surfaced a real data property: EEG061-064 are external channels with no scalp position, so distributed subsets automatically exclude the non-scalp externals (66 localized of 70). Full suite green.

Remaining (validation, not code)

Re-run the channel sweep on hallu with --montage to regenerate the Phase 3 topomaps as proper whole-head reduced montages (timing/equivalence numbers unchanged; only the maps become physically sensible). Tracked as the follow-up in this PR.

Tested

14 relevant tests pass (test_channel_selection, test_decompose_equiv, test_dimsweep_threads); ruff clean.

@neuromechanist

Copy link
Copy Markdown
Member Author

Review (Sonnet agents, 2 parallel reviewers)

Reviewed channel_selection.py + tests and the benchmark wiring (benchmark_decompose.py, benchmark_dimsweep.py, pyproject.toml). Findings addressed in d069e65:

Important (fixed)

  • select_distributed_channels(n<=0) returned 1 channel instead of an empty set. Added an explicit n<=0 guard.
  • Greedy selection could re-select an already-chosen index when candidates tie at distance 0 (coincident coordinates), silently breaking the unique-indices invariant. Now masks selected points out of the argmax, so the subset stays unique under degenerate geometry.
  • benchmark_dimsweep.py recorded/printed the requested nc rather than the actual selected count. When a montage has fewer localized electrodes than requested, it now records/prints len(sel), matching the fix already applied in benchmark_decompose.py. (Dormant under the default --channels 16,32,48,70, but real.)

Coverage (added)

  • Tests for n=0 (empty), coincident-coordinate uniqueness, centroid-seed membership, and the exact localized-row count from the real fixture. Suite: 9 passing, all on the real ds002718 electrode fixture (no synthetic geometry).

Suggestion (intentional, not changed)

  • benchmark_decompose.py npz filename embeds the requested nc while the npz body stores channels=len(sel)/channel_indices. Kept by design: the filename is the unique per-sweep-point handle (avoids collisions if two requested counts clip to the same localized count), and _compare groups by the authoritative npz body, not the filename. In every real sweep path len(sel)==nc, so they never disagree.

Verified correct (no change)

  • Topomap slicing chain (full[sel] -> n_ch count), channel_indices npz field + legacy np.arange fallback, EEG{i+1:03d} electrode-name mapping for non-contiguous subsets, lazy channel_selection import path, and timing invariance for the full-channel case.

@neuromechanist
neuromechanist merged commit f97594b into main Jul 11, 2026
7 checks passed
@neuromechanist
neuromechanist deleted the feature/issue-91-distributed-channels branch July 11, 2026 04:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Channel sweep should use spatially-distributed subsets, not the first N electrodes

1 participant